package gui;

import java.awt.Color;
import javax.swing.*;
import domain.player.Disc;
import domain.GameGrid;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.border.EmptyBorder;
import main.AppConstants;

/**
 * The GameBoard class renders the game's actual game board. It also handles
 * the disc animation when adding a new disc to the board.
 */
public class GameBoard extends JPanel {

	// Locks:
	private boolean animDone = true;
	private Lock lock = new ReentrantLock();
	private Condition animRunning = lock.newCondition();
	// Instances:
	private GUIController guiController;
	private JLayeredPane gameBoardPane;
	// Components:
	private JLabel board[][] = new JLabel[GameGrid.GRID_ROWS][GameGrid.GRID_COLUMNS];
	private final ImageIcon CELL_OVERLAY = new ImageIcon(getClass().getClassLoader().getResource(AppConstants.IMAGE_BOARD));
	private final ImageIcon DISC_EMPTY = new ImageIcon(getClass().getClassLoader().getResource(AppConstants.IMAGE_DISC_EMPTY));
	// Size of components:
	private final int CELL_SIZE = 80;

	/** GameBoard constructor */
	public GameBoard(GUIController guiController) {
		this.guiController = guiController;

		setLayout(new BorderLayout());

		createGameBoard();
	}

	/** Create game board */
	private void createGameBoard() {
		gameBoardPane = new JLayeredPane();
		gameBoardPane.setBorder(new EmptyBorder(0, 0, 0, 0));
		gameBoardPane.setLayout(new GridLayout(GameGrid.GRID_ROWS, GameGrid.GRID_COLUMNS, 0, 0));

		addBoardOverlay();
		addGameBoardCells();

		add(gameBoardPane);
	}

	/** Add the game board "frame" to the panel */
	private void addBoardOverlay() {
		setBackground(AppConstants.COLOR_BOARD_BACKGROUND);
		for (int i = 0; i < GameGrid.GRID_ROWS; i++) {
			for (int j = 0; j < GameGrid.GRID_COLUMNS; j++) {
				JLabel tempLabel = new JLabel();
				tempLabel.setIcon(CELL_OVERLAY);
				tempLabel.setBounds(j * CELL_SIZE, i * (CELL_SIZE - 2), CELL_SIZE, CELL_SIZE);
				add(tempLabel);
			}
		}
	}

	/** Add game board cells to the game board */
	private void addGameBoardCells() {
		for (int i = 0; i < GameGrid.GRID_ROWS; i++) {
			for (int j = 0; j < GameGrid.GRID_COLUMNS; j++) {
				JLabel cell = new JLabel();
				cell.setName("" + j);
				cell.setPreferredSize(new Dimension(CELL_SIZE, CELL_SIZE));
				cell.setBorder(BorderFactory.createEmptyBorder());
				cell.addMouseListener(new HumanListener(guiController.getLogicController()));
				board[i][j] = cell;
				gameBoardPane.add(cell);
			}
		}
	}

	/** Returns the game board data */
	public JLabel[][] getGameBoard() {
		return board;
	}

	/** Reset the game board */
	public void resetGameBoard() {
		JLabel cell;

		for (int i = 0; i < GameGrid.GRID_ROWS; i++) {
			for (int j = 0; j < GameGrid.GRID_COLUMNS; j++) {
				cell = (JLabel) board[i][j];
				cell.setIcon(DISC_EMPTY);
			}
		}
	}

	/** Add a disc to the game board usign an animation */
	public void animateDisc(int column) {
		lock.lock();
		try {
			while (animDone == false) {
				animRunning.await();
			}
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		} finally {
			lock.unlock();
		}
		animDone = false;
		new AnimateDisc(column, guiController.getCurrentPlayer().getDiscColor());
	}

	/** Updates the game board */
	public void updateGameBoard() {
		drawGameBoard();
	}

	/** Draw discs on the game board based on information from the game grid */
	private void drawGameBoard() {
		GameGrid currentGrid = guiController.getGameGrid();
		JLabel current;

		for (int i = 0; i < GameGrid.GRID_ROWS; i++) {
			for (int j = 0; j < GameGrid.GRID_COLUMNS; j++) {
				current = (JLabel) board[i][j];
				if (currentGrid.getDisc(i, j) == Disc.ONE) {
					current.setIcon(new DiscIcon(guiController.getPlayer(1).getDiscColor()));
				} else if (currentGrid.getDisc(i, j) == Disc.TWO) {
					current.setIcon(new DiscIcon(guiController.getPlayer(2).getDiscColor()));
				} else current.setIcon(DISC_EMPTY);
			}
		}
		repaint();
	}

	/** Presents the winner board */
	public void presentWinnerBoard(final Iterator winnerPointsItr) {
		List<Point> winnerCells = new ArrayList<Point>();
		while (winnerPointsItr.hasNext()) {
			winnerCells.add((Point) winnerPointsItr.next());
		}
		if (winnerCells.size() > 0) {
			flashWinnerCells(winnerCells);
		}
	}

	/** Flash the winner cells of the board. The method creates a new thread
	 *  for the "animation", so that it's smooth. */
	private void flashWinnerCells(final List<Point> winnerCells) {
		new Thread(new Runnable() {

			@Override
			public void run() {
				int count = 0;
				Color color = AppConstants.COLOR_DISC_FLASH, orgColor = null;
				while (count < AppConstants.NUM_WIN_FLASHES) {
					// Loop through the winnerRows iterator:
					for (int i = 0; i < winnerCells.size(); i++) {
						JLabel label = (JLabel) board[winnerCells.get(i).x][winnerCells.get(i).y];
						if (label.getIcon() instanceof DiscIcon) {
							DiscIcon disc = (DiscIcon) label.getIcon();
							if (orgColor == null) {
								orgColor = disc.getColor();
							}
							changeDiscColor(disc, color);
						}
					}
					repaint();
					count++;
					if (orgColor != null) {
						color = color.equals(orgColor)
								? AppConstants.COLOR_DISC_FLASH : orgColor;
					}
				}
			}
		}).start();
	}

	/** Changes the disc to a certain color */
	private void changeDiscColor(DiscIcon disc, Color color) {
		try {
			Thread.sleep(AppConstants.FLASH_DELAY);
		} catch (InterruptedException ex) {
			System.out.println("ex" + ex);
		}
		disc.setColor(color);
	}

	/** Returns the index for the first free cell in the provided column.
	 *  If the column is full -1 is returned. */
	private int getFreeRow(int column) {
		for (int row = GameGrid.GRID_ROWS - 1; row >= 0; row--) {
			if (board[row][column].getIcon().toString().endsWith(AppConstants.IMAGE_DISC_EMPTY)) {
				return row;
			}
		}
		return -1;
	}

	/**
	 * AnimatedDisc class allows animation for the disc when adding a new disc
	 * to the board. Meanwhile the animation is played the class locks certain
	 * methods so a client can't play multiple animations as the same time.
	 *
	 * The class is private because it needs the locks, but also needs to
	 * repaint the GameBoard upon animating the disc.
	 */
	private class AnimateDisc {

		// Column & disc:
		private int column;
		private JLabel animatedDisc;

		public AnimateDisc(int column, Color discColor) {
			this.column = column;
			animatedDisc = new JLabel(new DiscIcon(discColor));
			startAnimation();
		}

		/** Starts the animation */
		private void startAnimation() {
			// Lock:
			lock.lock();
			try {
				animDone = true;
				animRunning.signalAll();

				int row = getFreeRow(column);
				if (row > -1) {
					// 1. Add a new disc that we can animate:
					add(animatedDisc);

					// 2. Start & end positions:
					int xCoord = column * CELL_SIZE;				// column * CELL_SIZE
					double yCoord = 0;								// current Y coordinate of the animatedDisc
					int yCoordEnd = row * CELL_SIZE;				// relative px from the game board
					int distance = yCoordEnd - (int)yCoord;			// move distance [px]

					// 3. FPS independent movement speed:
					int animTime = AppConstants.FALL_TIME * 10000;	// animation time
					long oldTime = System.currentTimeMillis();		// start time
					long newTime = oldTime + 1;						// current time
					double pxFrame;
					double moves;

					// 4. Animation loop:
					while (yCoord < distance) {
						newTime = System.currentTimeMillis() + 1;
						moves = (double) (animTime / (newTime - oldTime));
						pxFrame = distance / moves;
						yCoord += pxFrame;

						animatedDisc.setBounds(xCoord, (int)yCoord, CELL_SIZE, CELL_SIZE);
						animatedDisc.repaint();
					}

					// 5. Remove animated disc:
					remove(animatedDisc);
				}
			} finally {
				lock.unlock();
			}
		}
	}
}